Setting up and Configuring Nginx with Docker Compose
I have often heard of Nginx, knowing primarily that it is used for reverse proxying. Previously, at work, I saw a Team Leader ask the operations team to set it up; when they were busy, the Team Leader handled it himself. It seems like a fundamental skill for senior engineers, so I wanted to study it a bit. As for the .NET query part of Elasticsearch, I'll start that in December.
As usual, I don't want to install it locally, so I'm using Docker to build the environment for easier testing and learning. However, after looking into it, I found that Nginx configuration is quite complex, so this note focuses mainly on the Docker Compose part, while also testing the Docker Compose V2 syntax.
Introduction to Nginx
Nginx (pronounced "engine-x") is a Web Server primarily used for:
- Serving static files: HTML, CSS, JavaScript, images, etc.
- Reverse Proxy: Forwarding requests to backend applications.
Basic Nginx Configuration
The main configuration file for Nginx is /etc/nginx/nginx.conf. When the container starts, it reads this file to determine Nginx's behavior.
Nginx Architecture Hierarchy
The Nginx configuration file consists of multiple contexts. Each context corresponds to a block, which can be divided into the following levels from outer to inner:
Main Context (Outermost)
Located at the outermost level of the configuration file, it defines global settings.
main context (outermost)
├── user nginx;
├── worker_processes auto;
├── error_log /var/log/nginx/error.log;
├── pid /run/nginx.pid;
│
├── events { } # Event processing settings
├── http { } # HTTP-related settings
├── mail { } # Mail proxy settings (optional)
└── stream { } # TCP/UDP proxy settings (optional)Purpose: To set execution parameters for the Nginx process, such as the number of workers, user permissions, and PID file location.
Events Context
Defines how Nginx handles connections.
Purpose: To set connection handling parameters, such as the number of connections each worker can handle and the event processing model.
HTTP Context
Defines global settings for the HTTP server.
http {
# HTTP global settings
│
├── map { } # Variable mapping (can have multiple)
│
├── server { } # Virtual host (can have multiple)
├── server { }
│
├── upstream { } # Backend server group (can have multiple)
└── upstream { }
}Purpose: To set global parameters related to HTTP, such as MIME types, log formats, and gzip compression. Settings defined at this level apply to all virtual hosts.
Common Blocks and Directives
Server Block
The server block defines a virtual host used to handle requests for specific domains or ports.
Placement: Can only be placed within the http block; multiple server blocks can be defined.
Basic Structure:
server {
listen 80; # Port to listen on
server_name example.com; # Domain name
location / { # Path matching rules
# Handling method
}
}Within the server block, you primarily use the location directive to define how different paths are handled.
Location Block
Matching Logic
location / uses prefix matching and will match all paths starting with /. Since all URL paths start with /, location / effectively matches all requests and is usually used as a fallback rule.
Basic Prefix Matching:
Nginx selects the most specific (longest) matching rule.
server {
listen 80;
server_name localhost;
location / {
# Lower priority, matches all paths
return 200 "Root path\n";
}
location /api/ {
# Higher priority, /api/ is more specific than /
return 200 "API path\n";
}
}Actual Operation:
Request: http://example.com/
→ Matches location /
Request: http://example.com/about.html
→ Matches location /
Request: http://example.com/api/users
→ Matches location /api/ (more specific)Full Matching Rules and Priority
Location supports various matching modifiers, with the following priority:
- Exact match
=: Matches only if identical (highest priority) - Prefix strong match
^~: Stops searching for regular expressions after a successful prefix match - Regular expression (case-sensitive)
~: Used in the order defined - Regular expression (case-insensitive)
~*: Used in the order defined - Normal prefix match: Longest one takes priority
- General match
/: Default rule (lowest priority)
Test Example:
server {
listen 80;
server_name localhost;
# Set default Content-Type
default_type "text/plain; charset=utf-8";
# 1. exact match (highest priority)
location = /test123 {
return 200 "exact_match\n";
}
# 2. prefix strong (stops regex search after match)
location ^~ /test999 {
return 200 "prefix_strong\n";
}
# 3. regex case-sensitive
location ~ ^/test[0-9]+$ {
return 200 "regex_sensitive\n";
}
# 4. regex case-insensitive
location ~* ^/TEST[0-9]+$ {
return 200 "regex_insensitive\n";
}
# 5. normal prefix
location /test {
return 200 "prefix_normal\n";
}
# 6. fallback (default rule)
location / {
return 200 "root\n";
}
}Test Results:
# Exact match (highest priority)
curl http://localhost/test123
→ "exact_match"
# Regex (case-sensitive)
curl http://localhost/test456
→ "regex_sensitive"
# Regex (case-insensitive)
curl http://localhost/TEST456
→ "regex_insensitive"
curl http://localhost/TEST789
→ "regex_insensitive"
# Prefix strong (stops regex search)
curl http://localhost/test999
→ "prefix_strong"
# Normal prefix
curl http://localhost/test
→ "prefix_normal"
curl http://localhost/testaaa
→ "prefix_normal"
# Default rule
curl http://localhost/other
→ "root"Testing matching rules:
# View Nginx loaded configuration and filter for location rules
nginx -T | grep -A5 "/test"WARNING
- If you define both
location /testandlocation ^~ /test, Nginx will throw an error due to rule conflict; you should choose one. - It is recommended to use the
curlcommand for testing, as some browsers automatically convert URL case based on history. For example, if you accesshttp://localhost/test123first, the browser might automatically converthttp://localhost/TEST123to lowercase.
Handling Methods
1. Serving static files
Read files directly from the file system and respond:
server {
listen 80;
server_name example.com;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ =404;
}
}Directive Explanation:
root: Specifies the root directory for static files.index: Specifies the default index file; when a directory is requested, it searches for these files in order.try_files: Attempts to find files or directories in order; returns a 404 error if none exist.
Operation Example:
Assuming the root directory structure is as follows:
/usr/share/nginx/html/
├── index.html
├── about.html
└── docs/
└── index.htmlRequest processing flow:
Request: http://example.com/
→ Reads /usr/share/nginx/html/index.html
Request: http://example.com/about.html
→ Reads /usr/share/nginx/html/about.html
Request: http://example.com/docs/
→ Reads /usr/share/nginx/html/docs/index.html
Request: http://example.com/notfound.html
→ Returns 404 error2. Reverse Proxy
Forward requests to a backend application using the proxy_pass directive:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://backend:8080;
}
}Proxy Pass Directive
proxy_pass is the core directive for Nginx as a reverse proxy, used to forward requests to a backend server.
Placement: Can only be placed within location, if in location, or limit_except blocks.
Basic Usage:
location / {
proxy_pass http://backend:8080;
}Impact of Trailing Slash:
Whether the proxy_pass URI contains a trailing slash affects forwarding behavior.
Case 1: proxy_pass has no URI (no trailing slash or path)
location /app/ {
# No path, full forwarding
proxy_pass http://backend;
}The complete original URI is passed to the backend:
Request: http://example.com/app/test
Forwarded: http://backend/app/testCase 2: proxy_pass has a URI (contains a path, even if just /)
location /app/ {
# Contains path /, performs replacement
proxy_pass http://backend/;
}The part matched by location is replaced by the proxy_pass URI:
Request: http://example.com/app/test
Forwarded: http://backend/test (/app/ replaced by /)Map Directive
The map directive is used to create custom variables, setting output variables based on the values of input variables.
map variables defined in the http block are global and can be used by all server blocks. If multiple map blocks are defined with the same output variable name, Nginx uses the last loaded definition, and previous ones are discarded.
Placement: Can only be placed within the http block.
Basic Syntax:
map $input_variable $output_variable {
value1 result1;
value2 result2;
default default_result;
}Simple Example:
http {
# Determine variable value based on request method
map $request_method $is_post {
POST "yes";
default "no";
}
}Using Regular Expressions:
map $http_user_agent $browser_type {
~Chrome "chrome"; # Case-sensitive, matches only Chrome
~*firefox "firefox"; # Case-insensitive, matches Firefox, firefox, FIREFOX
~*mobile "mobile"; # Case-insensitive, matches Mobile, mobile
default "default";
}Matching order is top-to-bottom; it stops at the first successful match.
About Variables:
In addition to using map to create custom variables, Nginx has many built-in variables, such as $remote_addr (client IP), $host (hostname), and $request_uri (request URI). For a complete list of variables, refer to the Alphabetical index of variables.
Default nginx.conf
You can use the following command to view the default nginx.conf:
docker run --rm nginx cat /etc/nginx/nginx.confBelow is the content of the default configuration file for the nginx:1.29.3 Docker image; other versions may vary slightly:
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf;
}Note the last line include /etc/nginx/conf.d/*.conf;, which means all .conf files placed in the /etc/nginx/conf.d/ directory will be loaded into the http block.
Actual Effect:
http {
# HTTP settings from nginx.conf
include /etc/nginx/mime.types;
access_log /var/log/nginx/access.log;
# --- Content of conf.d/default.conf is inserted here ---
server {
listen 80;
server_name localhost;
# ...
}
# --- Insertion ends ---
}Therefore, you only need to create website configuration files (e.g., default.conf) in the conf.d directory, and these settings will be automatically loaded into the http block. The default main context and events settings are usually sufficient.
Scenarios for Modifying nginx.conf
In most cases, you do not need to modify nginx.conf, unless you need to adjust the following settings:
Main Context Settings:
worker_processes: Number of worker processes.worker_rlimit_nofile: Number of files each worker can open.user: User executing the worker.
Events Block Settings:
worker_connections: Number of connections per worker.use: Event processing model (e.g., epoll).
If you need to modify these settings, you can use the following command to copy out the default nginx.conf:
docker run --rm nginx cat /etc/nginx/nginx.conf > volumes/config/nginx.confUsing a Configuration Generator
If you are unsure how to write the configuration file, you can use DigitalOcean's Nginx Config Generator to help generate a basic configuration file. This tool provides a graphical interface where you can select different configuration options based on your needs, such as:
- Static website or Reverse Proxy.
- SSL/HTTPS settings.
- PHP support.
- Compression and caching settings.
The generated configuration file can be copied and used directly, then fine-tuned according to actual requirements.
Creating Website Configuration in conf.d
When the Nginx Docker image starts, it has a built-in default.conf file in the /etc/nginx/conf.d/ directory as the default configuration. Below is the content of the default default.conf:
server {
listen 80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
# proxy the PHP scripts to Apache listening on 127.0.0.1:80
#
#location ~ \.php$ {
# proxy_pass http://127.0.0.1;
#}
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
#
#location ~ \.php$ {
# root html;
# fastcgi_pass 127.0.0.1:9000;
# fastcgi_index index.php;
# fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name;
# include fastcgi_params;
#}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
#location ~ /\.ht {
# deny all;
#}
}In practice, you will create your own configuration files to override or replace the default settings.
Directive Inheritance and Overriding
Many Nginx directives can be set at different levels, and child-level settings override parent-level settings. Taking log settings as an example:
access_log and error_log can be set at multiple levels:
| Directive | Available Locations |
|---|---|
error_log | main, http, mail, stream, server, location |
access_log | http, server, location, if in location, limit_except |
If you set the log path in server or location, it will override the upper-level settings. If not set, it inherits the settings from the upper level (http or main).
Example:
# nginx.conf
http {
access_log /var/log/nginx/access.log; # Default path
# conf.d/site-a.conf
server {
server_name site-a.com;
access_log /var/log/nginx/site-a.log; # Override, use independent log
}
# conf.d/site-b.conf
server {
server_name site-b.com;
# Not set, inherits /var/log/nginx/access.log from http
}
}Cross-file Sharing of Map Variables
map variables defined in the http block are global and can be used by all server blocks. Since Nginx loads all files in conf.d/*.conf, map variables defined in different conf files are shared in the same namespace.
Nginx loads conf files in alphabetical order of filenames. Since duplicate definitions of the same output variable will be overwritten, it is recommended to centralize all map definitions in a single independent conf file to avoid unexpected overwriting issues.
File Structure Example:
volumes/config/conf.d/
├── maps.conf # All map definitions
├── api.conf # API service settings
└── default.conf # Default website settingsImplementation Examples
Static Website Example
Create Data Directories
First, create the necessary data directories to store Nginx configuration files, website files, and log files.
# Create data directories
mkdir -p volumes/config/conf.d
mkdir -p volumes/html
mkdir -p volumes/logsBasic Docker Compose Configuration
Create a compose.yaml file:
services:
nginx:
image: nginx:latest
container_name: nginx
restart: always
ports:
- "80:80"
volumes:
# Uncomment the line below if you need to override main context settings
# - ./volumes/config/nginx.conf:/etc/nginx/nginx.conf:ro
- ./volumes/config/conf.d:/etc/nginx/conf.d:ro
- ./volumes/html:/usr/share/nginx/html:ro
- ./volumes/logs:/var/log/nginx
environment:
- TZ=Asia/TaipeiVolumes Mount Explanation
volumes:
- ./volumes/config/conf.d:/etc/nginx/conf.d:ro # Configuration files (read-only)
- ./volumes/html:/usr/share/nginx/html:ro # Static files (read-only)
- ./volumes/logs:/var/log/nginx # Log directory (needs write access)Use of :ro (read-only):
Why add :ro?
- Prevents processes inside the container from accidentally modifying host files.
- Protects configuration files from tampering.
- Clearly expresses that these directories are for reading only.
For example, when the container starts, Nginx checks if default.conf is the official default configuration file; if so, it attempts to add listen [::]:80; to support IPv6. Adding :ro prevents this modification.
What should not have :ro?
- Log directory: Nginx must be able to write logs.
- Upload directory: If the website allows users to upload files.
- Cache directory: Nginx needs to write cache data.
Create Website Configuration File
Create default.conf in the volumes/config/conf.d directory:
server {
listen 80; # Listen on IPv4
listen [::]:80; # Listen on IPv6
server_name localhost;
# Set root directory
root /usr/share/nginx/html;
index index.html index.htm;
# Character encoding
charset utf-8;
# Log paths
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# Main location settings
location / {
try_files $uri $uri/ =404;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
}Create Test Web Page
Create index.html in the volumes/html directory:
<!DOCTYPE html>
<html lang="zh-TW">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nginx Test Page</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 40px;
border-radius: 8px;
text-align: center;
}
h1 {
color: #2c3e50;
margin-bottom: 20px;
}
p {
color: #555;
line-height: 1.6;
}
</style>
</head>
<body>
<div class="container">
<h1>✓ Nginx Running Successfully</h1>
<p>This is an Nginx test environment set up using Docker Compose</p>
</div>
</body>
</html>Verify Web Page
Run the following command to start the container:
docker compose up -dEnter http://localhost/ in your browser to see the test page.
Testing and Reloading Configuration
After modifying the configuration file, you can use the following commands in the running container to check the syntax, avoiding service failure due to configuration errors.
# Test configuration file syntax
docker compose exec nginx nginx -t
# Reload configuration file (without interrupting service)
docker compose exec nginx nginx -s reloadIt is recommended to test the syntax with nginx -t first, and then execute nginx -s reload to reload after confirming it is correct.
Reverse Proxy Example
Forward requests to a backend application. For official Reverse Proxy example documentation, refer to NGINX Reverse Proxy and WebSocket proxying.
Below is an integrated reference example:
volumes/config/conf.d/maps.conf:
# WebSocket support: Define Connection upgrade variable
map $http_upgrade $connection_upgrade {
'' close;
default upgrade;
}volumes/config/conf.d/default.conf:
server {
listen 80;
listen [::]:80;
server_name localhost;
location / {
# Forward to the application in the web container
proxy_pass http://web:8080/;
# === Basic Headers (Required) ===
# Pass original hostname
proxy_set_header Host $host;
# Pass real client IP
proxy_set_header X-Real-IP $remote_addr;
# Pass client IP chain (if passing through multiple proxies)
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Pass original protocol (http or https)
proxy_set_header X-Forwarded-Proto $scheme;
# === Full Forwarding Information (Optional) ===
# Pass original hostname (specifically for X-Forwarded series)
# proxy_set_header X-Forwarded-Host $host;
# Pass original port
# Note: $server_port is the port Nginx is listening on
# proxy_set_header X-Forwarded-Port $server_port;
# === WebSocket Support (Optional) ===
# WebSocket requires HTTP/1.1 protocol
# proxy_http_version 1.1;
# Pass Upgrade Header
# proxy_set_header Upgrade $http_upgrade;
# Set Connection based on whether Upgrade Header exists
# proxy_set_header Connection $connection_upgrade;
# WebSocket long connection timeout (default 60 seconds, set to 24 hours here)
# proxy_read_timeout 86400s;
# === Request Tracking (Optional) ===
# Pass unique request ID for log tracking and troubleshooting
# Requires Nginx 1.11.0+
# proxy_set_header X-Request-ID $request_id;
# === Timeout Settings (Optional) ===
# Timeout for connecting to the application (default 60 seconds)
# May need adjustment if the application starts slowly or the network is unstable
# proxy_connect_timeout 60s;
# Timeout for sending requests to the application (default 60 seconds)
# Need to increase this value if uploading large files
# proxy_send_timeout 60s;
# Timeout for reading responses from the application (default 60 seconds)
# Need to adjust this value if the application takes longer to process (report generation, data export, etc.)
# Note: If WebSocket is enabled, use the proxy_read_timeout 86400s above
# proxy_read_timeout 60s;
}
}Docker Compose with Web Service
services:
nginx:
image: nginx:latest
container_name: nginx
ports:
- "80:80"
volumes:
- ./volumes/config/conf.d:/etc/nginx/conf.d:ro
depends_on:
- web
environment:
- TZ=Asia/Taipei
web:
image: mcr.microsoft.com/dotnet/samples:aspnetapp
container_name: web
environment:
- TZ=Asia/Taipei
# Do not expose ports, only accessible internally by NginxHere, the sample image provided by .NET is used directly. For details, refer to the official GitHub .NET container samples.
This image is a Razor Pages web application. After the container starts, enter http://localhost/ in your browser to view the web page content.
Template and Environment Variable Example
The official Nginx image has supported template functionality since version 1.19. For detailed usage, refer to the Official Nginx Docker Hub.
This feature allows users to focus on a few settings by defining template files and using environment variables in Docker Compose.
Modify Docker Compose
services:
nginx:
image: nginx:latest
ports:
- "80:80"
volumes:
- ./volumes/templates:/etc/nginx/templates
- ./volumes/config/templates.d:/etc/nginx/conf.d # Used to view generated conf files
environment:
- TZ=Asia/Taipei
- NGINX_HOST=localhost
- NGINX_PORT=80Create Template File
Create default.conf.template in the volumes/templates directory:
server {
listen ${NGINX_PORT};
server_name ${NGINX_HOST};
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}When the container starts, it automatically replaces environment variables in the configuration file, resulting in the following:
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
}Precautions
When using the template method, environment variable replacement only occurs when the container starts. Therefore:
- ✅ After modifying environment variables, you need to restart the container:
docker compose up -d - ❌ You cannot use
docker compose exec nginx nginx -s reloadto reload the configuration.
If you need to adjust settings frequently, it is recommended to use the direct mounting of conf files.
Reference Resources
Change Log
- 2025-11-27 Initial version created.
